using System;
using System.Collections.Generic;
using System.Linq;
using System.Linq.Expressions;
using System.Reflection;
using log4net;
using Microsoft.SharePoint;
using SharePoint.Orm.Attributes;
using SharePoint.Orm.Caml;
using SharePoint.Orm.Helpers;

namespace SharePoint.Orm {
    public class SPQueryContext {
        private static readonly ILog _log = LogManager.GetLogger(typeof(SPQueryContext));
        public static object Execute(SPDataContext ctx, Expression expression, bool isEnumerable) {
            // The expression must represent a query over the data source.
            if (!IsQueryOverDataSource(expression)) {
                throw new InvalidProgramException("No query over the data source was specified.");
            }

            _log.Debug("Executing expression: " + expression);
            ExecutionResult result = new ExecutionResult {
                    RootExpression = expression
                };
                ExecuteRecursive(ctx, result, expression);

            if (result.QueryResults == null) {
                result.QueryResults = GetListItemEnumerable(ctx, result);
                result.RootExpression = ConstantExpression.Constant(result.QueryResults);
            }

            // This step creates an IQueryable that executes by replacing Queryable methods with Enumerable methods.
            if (isEnumerable) {
                return result.QueryResults.Provider.CreateQuery(result.RootExpression);
            }
            return result.QueryResults.Provider.Execute(result.RootExpression);
        }

        private static void ExecuteRecursive(SPDataContext ctx, ExecutionResult result, Expression expression) {
            if (result.Done) {
                return;
            }
            if (expression is MethodCallExpression) {
                MethodCallExpression methodCallExpression = (MethodCallExpression)expression;
                ExecuteRecursive(ctx, result, methodCallExpression.Arguments[0]);
                string methodName = methodCallExpression.Method.Name;
                switch (methodName) {
                    case "OfType":
                    ExecuteWhereTypeId(result, methodCallExpression);
                    break;
                    case "Where":
                    ExecuteWhere(ctx, result, methodCallExpression);
                    break;
                    default:
                    ExecuteUnknownMethod(ctx, result, methodCallExpression);
                    break;
                }
            } else if (expression is ConstantExpression) {
                ConstantExpression constantExpression = (ConstantExpression)expression;
                result.DataContext = ((SPQueryable)constantExpression.Value).DataContext;
                result.QueryResultsOrmType = result.ListOrmType = constantExpression.Value.GetType().GetGenericArguments()[0];
                SPOrmListAttribute listAttr = SPOrmAttributeHelper.GetListAttribute(result.ListOrmType);
                string listName = listAttr.Title;
                result.List = ctx.Site.Lists[listName];
            } else {
                throw new InvalidQueryException("Could not parse query", expression);
            }
        }

        static void ExecuteWhere(SPDataContext ctx, ExecutionResult result, MethodCallExpression expression) {
            LambdaExpression whereExpr = (LambdaExpression)((UnaryExpression)expression.Arguments[1]).Operand;
            whereExpr = (LambdaExpression)Evaluator.PartialEval(whereExpr);
            ExpressionType whereType = whereExpr.Body.NodeType;
            if (whereExpr.Body is BinaryExpression && (whereType == ExpressionType.Equal || whereType == ExpressionType.NotEqual)) {
                BinaryExpression equalsExpr = (BinaryExpression)whereExpr.Body;
                Expression left = equalsExpr.Left;
                Expression right = equalsExpr.Right;
                if (left is MemberExpression && right is ConstantExpression) {
                    MemberExpression member = (MemberExpression)left;
                    ConstantExpression constant = (ConstantExpression)right;
                    SPOrmIdFieldAttribute idFieldAttr = (SPOrmIdFieldAttribute)member.Member.GetCustomAttributes(typeof(SPOrmIdFieldAttribute), true).FirstOrDefault();
                    if (idFieldAttr != null) {
                        if(whereType == ExpressionType.Equal)
                            result.CamlQuery.AppendWhere(new CamlEquals("ID", CamlValueType.Counter, constant.Value.ToString()));
                        else 
                            result.CamlQuery.AppendWhere(new CamlNotEquals("ID", CamlValueType.Counter, constant.Value.ToString()));
                        return;
                    }
                }
            }
            ExecuteUnknownMethod(ctx, result, expression);
        }


        private static void ExecuteUnknownMethod(SPDataContext ctx, ExecutionResult result, MethodCallExpression expression) {
            result.Done = true;
            result.QueryResults = GetListItemEnumerable(ctx, result);
            Expression newExpr = ConstantExpression.Constant(result.QueryResults);
            Expression from = expression.Arguments[0];
            ExpressionTreeModifier exprTreeModifier = new ExpressionTreeModifier(from, newExpr);
            result.RootExpression = exprTreeModifier.CopyAndModify(result.RootExpression);
        }

        private static IQueryable GetListItemEnumerable(SPDataContext ctx, ExecutionResult result) {
            Type methodGenericType = result.QueryResultsOrmType;
            SPQuery query = result.GetQuery();
            _log.Debug("Using caml query:\n" + query.Query);
            SPListItemCollection listItems = result.List.GetItems(query);
            _log.Debug("Query returned " + listItems.Count + " items");
            object listItemEnum = Activator.CreateInstance(
                typeof(SPListItemConverterEnumerable<>).MakeGenericType(methodGenericType),
                new object[] { ctx, listItems });
            return ToQueryable(methodGenericType, listItemEnum);
        }

        private static IQueryable ToQueryable(Type genericType, object obj) {
            MethodInfo asQueryableMethodInfo = typeof(Queryable).GetMethods().Single(mi => mi.Name == "AsQueryable" && mi.ContainsGenericParameters);
            asQueryableMethodInfo = asQueryableMethodInfo.MakeGenericMethod(genericType);
            return (IQueryable)asQueryableMethodInfo.Invoke(null, new[] { obj });
        }

        private static void ExecuteWhereTypeId(ExecutionResult result, MethodCallExpression expression) {
            Type arg = expression.Method.GetGenericArguments()[0];
            SPOrmContentTypeAttribute contentTypeAttr = SPOrmAttributeHelper.GetContentTypeAttribute(arg);
            string contentType = contentTypeAttr.Name;
            result.QueryResultsOrmType = arg;
            CamlEquals @equals = new CamlEquals("ContentType", CamlValueType.Text, contentType);
            result.CamlQuery.AppendWhere(@equals);
        }

        private static bool IsQueryOverDataSource(Expression expression) {
            return (expression is MethodCallExpression) || (expression is ConstantExpression);
        }

        private class ExecutionResult {
            public SPList List { get; set; }
            public SPDataContext DataContext { get; set; }
            public Type ListOrmType { get; set; }
            public CamlQuery CamlQuery { get; private set; }
            public bool Done { get; set; }
            public Expression RootExpression { get; set; }
            public Type QueryResultsOrmType { get; set; }
            public IQueryable QueryResults { get; set; }

            public ExecutionResult() {
                CamlQuery = new CamlQuery();
            }

            public SPQuery GetQuery() {
                string caml = CamlQuery.ToString(false);
                SPQuery query = new SPQuery {
                    Query = caml
                };
                return query;
            }
        }
    }
}